/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var _config = require("../../config");

var __DEV__ = _config.__DEV__;

var _util = require("static/plugins/js/zrender/lib/core/util");

var isTypedArray = _util.isTypedArray;
var extend = _util.extend;
var assert = _util.assert;
var each = _util.each;
var isObject = _util.isObject;

var _model = require("../../util/model");

var getDataItemValue = _model.getDataItemValue;
var isDataItemOption = _model.isDataItemOption;

var _number = require("../../util/number");

var parseDate = _number.parseDate;

var Source = require("../Source");

var _sourceType = require("./sourceType");

var SOURCE_FORMAT_TYPED_ARRAY = _sourceType.SOURCE_FORMAT_TYPED_ARRAY;
var SOURCE_FORMAT_ARRAY_ROWS = _sourceType.SOURCE_FORMAT_ARRAY_ROWS;
var SOURCE_FORMAT_ORIGINAL = _sourceType.SOURCE_FORMAT_ORIGINAL;
var SOURCE_FORMAT_OBJECT_ROWS = _sourceType.SOURCE_FORMAT_OBJECT_ROWS;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
// TODO
// ??? refactor? check the outer usage of data provider.
// merge with defaultDimValueGetter?

/**
 * If normal array used, mutable chunk size is supported.
 * If typed array used, chunk size must be fixed.
 */
function DefaultDataProvider(source, dimSize) {
    if (!Source.isInstance(source)) {
        source = Source.seriesDataToSource(source);
    }

    this._source = source;
    var data = (this._data = source.data);
    var sourceFormat = source.sourceFormat; // Typed array. TODO IE10+?

    if (sourceFormat === SOURCE_FORMAT_TYPED_ARRAY) {
        this._offset = 0;
        this._dimSize = dimSize;
        this._data = data;
    }

    var methods =
        providerMethods[
            sourceFormat === SOURCE_FORMAT_ARRAY_ROWS
                ? sourceFormat + "_" + source.seriesLayoutBy
                : sourceFormat
        ];
    extend(this, methods);
}

var providerProto = DefaultDataProvider.prototype; // If data is pure without style configuration

providerProto.pure = false; // If data is persistent and will not be released after use.

providerProto.persistent = true; // ???! FIXME legacy data provider do not has method getSource

providerProto.getSource = function () {
    return this._source;
};

var providerMethods = {
    arrayRows_column: {
        pure: true,
        count: function () {
            return Math.max(0, this._data.length - this._source.startIndex);
        },
        getItem: function (idx) {
            return this._data[idx + this._source.startIndex];
        },
        appendData: appendDataSimply,
    },
    arrayRows_row: {
        pure: true,
        count: function () {
            var row = this._data[0];
            return row ? Math.max(0, row.length - this._source.startIndex) : 0;
        },
        getItem: function (idx) {
            idx += this._source.startIndex;
            var item = [];
            var data = this._data;

            for (var i = 0; i < data.length; i++) {
                var row = data[i];
                item.push(row ? row[idx] : null);
            }

            return item;
        },
        appendData: function () {
            throw new Error(
                'Do not support appendData when set seriesLayoutBy: "row".'
            );
        },
    },
    objectRows: {
        pure: true,
        count: countSimply,
        getItem: getItemSimply,
        appendData: appendDataSimply,
    },
    keyedColumns: {
        pure: true,
        count: function () {
            var dimName = this._source.dimensionsDefine[0].name;
            var col = this._data[dimName];
            return col ? col.length : 0;
        },
        getItem: function (idx) {
            var item = [];
            var dims = this._source.dimensionsDefine;

            for (var i = 0; i < dims.length; i++) {
                var col = this._data[dims[i].name];
                item.push(col ? col[idx] : null);
            }

            return item;
        },
        appendData: function (newData) {
            var data = this._data;
            each(newData, function (newCol, key) {
                var oldCol = data[key] || (data[key] = []);

                for (var i = 0; i < (newCol || []).length; i++) {
                    oldCol.push(newCol[i]);
                }
            });
        },
    },
    original: {
        count: countSimply,
        getItem: getItemSimply,
        appendData: appendDataSimply,
    },
    typedArray: {
        persistent: false,
        pure: true,
        count: function () {
            return this._data ? this._data.length / this._dimSize : 0;
        },
        getItem: function (idx, out) {
            idx = idx - this._offset;
            out = out || [];
            var offset = this._dimSize * idx;

            for (var i = 0; i < this._dimSize; i++) {
                out[i] = this._data[offset + i];
            }

            return out;
        },
        appendData: function (newData) {
            this._data = newData;
        },
        // Clean self if data is already used.
        clean: function () {
            // PENDING
            this._offset += this.count();
            this._data = null;
        },
    },
};

function countSimply() {
    return this._data.length;
}

function getItemSimply(idx) {
    return this._data[idx];
}

function appendDataSimply(newData) {
    for (var i = 0; i < newData.length; i++) {
        this._data.push(newData[i]);
    }
}

var rawValueGetters = {
    arrayRows: getRawValueSimply,
    objectRows: function (dataItem, dataIndex, dimIndex, dimName) {
        return dimIndex != null ? dataItem[dimName] : dataItem;
    },
    keyedColumns: getRawValueSimply,
    original: function (dataItem, dataIndex, dimIndex, dimName) {
        // FIXME
        // In some case (markpoint in geo (geo-map.html)), dataItem
        // is {coord: [...]}
        var value = getDataItemValue(dataItem);
        return dimIndex == null || !(value instanceof Array)
            ? value
            : value[dimIndex];
    },
    typedArray: getRawValueSimply,
};

function getRawValueSimply(dataItem, dataIndex, dimIndex, dimName) {
    return dimIndex != null ? dataItem[dimIndex] : dataItem;
}

var defaultDimValueGetters = {
    arrayRows: getDimValueSimply,
    objectRows: function (dataItem, dimName, dataIndex, dimIndex) {
        return converDataValue(
            dataItem[dimName],
            this._dimensionInfos[dimName]
        );
    },
    keyedColumns: getDimValueSimply,
    original: function (dataItem, dimName, dataIndex, dimIndex) {
        // Performance sensitive, do not use modelUtil.getDataItemValue.
        // If dataItem is an plain object with no value field, the var `value`
        // will be assigned with the object, but it will be tread correctly
        // in the `convertDataValue`.
        var value =
            dataItem && (dataItem.value == null ? dataItem : dataItem.value); // If any dataItem is like { value: 10 }

        if (!this._rawData.pure && isDataItemOption(dataItem)) {
            this.hasItemOption = true;
        }

        return converDataValue(
            value instanceof Array
                ? value[dimIndex] // If value is a single number or something else not array.
                : value,
            this._dimensionInfos[dimName]
        );
    },
    typedArray: function (dataItem, dimName, dataIndex, dimIndex) {
        return dataItem[dimIndex];
    },
};

function getDimValueSimply(dataItem, dimName, dataIndex, dimIndex) {
    return converDataValue(dataItem[dimIndex], this._dimensionInfos[dimName]);
}
/**
 * This helper method convert value in data.
 * @param {string|number|Date} value
 * @param {Object|string} [dimInfo] If string (like 'x'), dimType defaults 'number'.
 *        If "dimInfo.ordinalParseAndSave", ordinal value can be parsed.
 */

function converDataValue(value, dimInfo) {
    // Performance sensitive.
    var dimType = dimInfo && dimInfo.type;

    if (dimType === "ordinal") {
        // If given value is a category string
        var ordinalMeta = dimInfo && dimInfo.ordinalMeta;
        return ordinalMeta ? ordinalMeta.parseAndCollect(value) : value;
    }

    if (
        dimType === "time" && // spead up when using timestamp
        typeof value !== "number" &&
        value != null &&
        value !== "-"
    ) {
        value = +parseDate(value);
    } // dimType defaults 'number'.
    // If dimType is not ordinal and value is null or undefined or NaN or '-',
    // parse to NaN.

    return value == null || value === ""
        ? NaN // If string (like '-'), using '+' parse to NaN
        : // If object, also parse to NaN
          +value;
} // ??? FIXME can these logic be more neat: getRawValue, getRawDataItem,
// Consider persistent.
// Caution: why use raw value to display on label or tooltip?
// A reason is to avoid format. For example time value we do not know
// how to format is expected. More over, if stack is used, calculated
// value may be 0.91000000001, which have brings trouble to display.
// TODO: consider how to treat null/undefined/NaN when display?

/**
 * @param {module:echarts/data/List} data
 * @param {number} dataIndex
 * @param {string|number} [dim] dimName or dimIndex
 * @return {Array.<number>|string|number} can be null/undefined.
 */

function retrieveRawValue(data, dataIndex, dim) {
    if (!data) {
        return;
    } // Consider data may be not persistent.

    var dataItem = data.getRawDataItem(dataIndex);

    if (dataItem == null) {
        return;
    }

    var sourceFormat = data.getProvider().getSource().sourceFormat;
    var dimName;
    var dimIndex;
    var dimInfo = data.getDimensionInfo(dim);

    if (dimInfo) {
        dimName = dimInfo.name;
        dimIndex = dimInfo.index;
    }

    return rawValueGetters[sourceFormat](
        dataItem,
        dataIndex,
        dimIndex,
        dimName
    );
}
/**
 * Compatible with some cases (in pie, map) like:
 * data: [{name: 'xx', value: 5, selected: true}, ...]
 * where only sourceFormat is 'original' and 'objectRows' supported.
 *
 * ??? TODO
 * Supported detail options in data item when using 'arrayRows'.
 *
 * @param {module:echarts/data/List} data
 * @param {number} dataIndex
 * @param {string} attr like 'selected'
 */

function retrieveRawAttr(data, dataIndex, attr) {
    if (!data) {
        return;
    }

    var sourceFormat = data.getProvider().getSource().sourceFormat;

    if (
        sourceFormat !== SOURCE_FORMAT_ORIGINAL &&
        sourceFormat !== SOURCE_FORMAT_OBJECT_ROWS
    ) {
        return;
    }

    var dataItem = data.getRawDataItem(dataIndex);

    if (sourceFormat === SOURCE_FORMAT_ORIGINAL && !isObject(dataItem)) {
        dataItem = null;
    }

    if (dataItem) {
        return dataItem[attr];
    }
}

exports.DefaultDataProvider = DefaultDataProvider;
exports.defaultDimValueGetters = defaultDimValueGetters;
exports.retrieveRawValue = retrieveRawValue;
exports.retrieveRawAttr = retrieveRawAttr;
